/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

/*
 * WorldWindFrame.java
 *
 * Created on 14.10.2011, 18:37:20
 */
package com.transas.zorlog.worldwind;

import com.transas.zorlog.data.DataSource;
import com.transas.zorlog.data.Retriever;
import gov.nasa.worldwind.Configuration;
import gov.nasa.worldwind.Model;
import gov.nasa.worldwind.WorldWind;
import gov.nasa.worldwind.avlist.AVKey;
import gov.nasa.worldwind.awt.WorldWindowGLCanvas;
import gov.nasa.worldwind.event.RenderingEvent;
import gov.nasa.worldwind.event.RenderingListener;
import gov.nasa.worldwind.geom.Angle;
import gov.nasa.worldwind.geom.Position;
import gov.nasa.worldwind.globes.Globe;
import gov.nasa.worldwind.layers.Layer;
import gov.nasa.worldwind.layers.LayerList;
import gov.nasa.worldwind.layers.MarkerLayer;
import gov.nasa.worldwind.layers.RenderableLayer;
import gov.nasa.worldwind.render.BasicShapeAttributes;
import gov.nasa.worldwind.render.Material;
import gov.nasa.worldwind.render.Path;
import gov.nasa.worldwind.render.Renderable;
import gov.nasa.worldwind.render.markers.*;
import gov.nasa.worldwind.view.firstperson.BasicFlyView;
import gov.nasa.worldwind.view.orbit.BasicOrbitView;
import java.awt.BorderLayout;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;

/**
 *
 * @author amalikov
 */
public class WorldWindFrame extends javax.swing.JFrame {

	/** Creates new form WorldWindFrame */
	public WorldWindFrame(String homeDir, Rectangle bounds, ActionListener listener) {
		this.homeDir = homeDir;
		this.listener = listener;
		
		initComponents();
		setBounds(bounds);
		
		try {
			System.setProperty("sun.awt.noerasebackground", "true");	// This is to stop the AWT canvas from flickering
			System.setProperty("gov.nasa.worldwind.app.config.document", homeDir + "/data/config/worldwind.xml");
		} 
		catch (NoSuchMethodError e) {}
		
		initWWD();
		viewGuard = new ViewGuard();		
		updateLayersVisibility();
		//printLayers();
	}

	public void setOptions(String latitude, String longitude, String altitude, double offset, String roll, String pitch, String heading) {
		this.latitude = latitude;
		this.longitude = longitude;
		this.altitude = altitude;
		this.offset = offset;
		this.roll = roll;
		this.pitch = pitch;
		this.heading = heading;
	}
	
	public void setTrack(DataSource ds, boolean lookAtTrack) {
		RenderableLayer layer = getTrackLayer();

		if (layer == null)
			return;
		
		List<Position> pathPositions = new ArrayList<>();
		
		Retriever lat = new Retriever(latitude);
		Retriever lon = new Retriever(longitude);
		Retriever alt = new Retriever(altitude, 1, null, offset);
		
		double minLat = lat.get(ds, 0);
		double maxLat = minLat;
		double minLon = lon.get(ds, 0);
		double maxLon = minLon;
		double maxAlt = alt.get(ds, 0);
		
		final int size = ds.getSize();
		for (int i = 0; i < size; ++i) {
			final double latValue = lat.get(ds, i);
			final double lonValue = lon.get(ds, i);
			final double altValue = alt.get(ds, i);
			minLat = Math.min(minLat, latValue);
			maxLat = Math.max(maxLat, latValue);
			minLon = Math.min(minLon, lonValue);
			maxLon = Math.max(maxLon, lonValue);
			maxAlt = Math.max(maxAlt, altValue);
			pathPositions.add(Position.fromDegrees(latValue, lonValue, altValue));
		}

		Path path = new Path(pathPositions);
		path.setDrawVerticals(false);
		path.setExtrude(menuViewTrackExtrude.getState());
		
		BasicShapeAttributes pathAttr = new BasicShapeAttributes();
		pathAttr.setInteriorMaterial(Material.RED);
		pathAttr.setInteriorOpacity(0.25);
		pathAttr.setOutlineMaterial(Material.RED);
		path.setAttributes(pathAttr);

		layer.removeAllRenderables();
		layer.addRenderable(path);
		
		if (lookAtTrack) {
			trackBounds = null;
			if (size > 0) {
				trackBounds = new Bounds(minLat, maxLat, minLon, maxLon, maxAlt);
				lookToTrack(true);
			}
		}
		
		menuViewTrack.setState(true);
	}

	
	public void setCursor(DataSource ds, int index) {
		MarkerLayer layer = getMarkerLayer();
		
		if (layer == null || !viewGuard.isOk())
			return;
		
		final double lat = ds.getValue(ds.getHandle(latitude), index);
		final double lon = ds.getValue(ds.getHandle(longitude), index);
		final double alt = ds.getValue(ds.getHandle(altitude), index) + offset;
		final double hdg = ds.getValue(ds.getHandle(heading), index);
		final double rol = ds.getValue(ds.getHandle(roll), index);
		final double pch = ds.getValue(ds.getHandle(pitch), index);
		
		MarkerAttributes attr = new BasicMarkerAttributes(Material.WHITE, BasicMarkerShape.ORIENTED_SPHERE, 1);
		attr.setHeadingMaterial(Material.GREEN);
		attr.setHeadingScale(2);
				
		List<Marker> markers = new ArrayList<>();
		markers.add(new BasicMarker(Position.fromDegrees(lat, lon, alt), attr, Angle.fromDegrees(hdg)));
		
		layer.setMarkers(markers);
		
		if (menuViewFPV.getState() != firstPersonView) {
			firstPersonView = menuViewFPV.getState();
			if (firstPersonView) {
				wwd.setView(new BasicFlyView());
				((BasicFlyView)wwd.getView()).setViewInputHandler(new NullViewInputHandler());
			}
			else {
				wwd.setView(new BasicOrbitView());
				viewGuard = new ViewGuard();
			}
		}
		
		if (firstPersonView) {
			BasicFlyView view = (BasicFlyView)wwd.getView();
			view.setEyePosition(Position.fromDegrees(lat, lon, alt));
			view.setHeading(Angle.fromDegrees(hdg));
			view.setRoll(Angle.fromDegrees(rol));
			view.setPitch(Angle.fromDegrees(pch + 90));
		} else if (menuViewFollow.getState()) {
			BasicOrbitView view = (BasicOrbitView)wwd.getView();
			if (!view.isAnimating())
				view.setCenterPosition(Position.fromDegrees(lat, lon, alt));
		}
		
		wwd.redrawNow();
	}
	
	
	private RenderableLayer getTrackLayer() {
		if (wwd == null)
			return null;
		
		RenderableLayer layer = (RenderableLayer)wwd.getModel().getLayers().getLayerByName("Track");

		if (layer == null) {
			layer = new RenderableLayer();
			layer.setName("Track");
			layer.setPickEnabled(false);
			insertLayer(layer);
		}
		
		return layer;
	}

	private MarkerLayer getMarkerLayer() {
		if (wwd == null)
			return null;
		
		MarkerLayer layer = (MarkerLayer)wwd.getModel().getLayers().getLayerByName("Cursor");
		
		if (layer == null) {
			layer = new MarkerLayer();
			layer.setName("Cursor");
			layer.setPickEnabled(false);
			insertLayer(layer);
		}
		
		return layer;
	}
	
	
	/** This method is called from within the constructor to
	 * initialize the form.
	 * WARNING: Do NOT modify this code. The content of this method is
	 * always regenerated by the Form Editor.
	 */
	@SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        javax.swing.JMenuBar menu = new javax.swing.JMenuBar();
        javax.swing.JMenu menuView = new javax.swing.JMenu();
        menuViewOSM = new javax.swing.JCheckBoxMenuItem();
        menuViewTrack = new javax.swing.JCheckBoxMenuItem();
        menuViewTrackExtrude = new javax.swing.JCheckBoxMenuItem();
        javax.swing.JPopupMenu.Separator jSeparator1 = new javax.swing.JPopupMenu.Separator();
        menuViewFollow = new javax.swing.JCheckBoxMenuItem();
        menuViewFPV = new javax.swing.JCheckBoxMenuItem();
        javax.swing.JMenu menuTools = new javax.swing.JMenu();
        javax.swing.JMenuItem menuToolsMeasure = new javax.swing.JMenuItem();

        setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);
        setTitle("Map (ZorLog)");
        setMinimumSize(new java.awt.Dimension(400, 300));
        setName("WorldWind"); // NOI18N
        addWindowListener(new java.awt.event.WindowAdapter() {
            public void windowClosed(java.awt.event.WindowEvent evt) {
                formWindowClosed(evt);
            }
        });
        addComponentListener(new java.awt.event.ComponentAdapter() {
            public void componentMoved(java.awt.event.ComponentEvent evt) {
                formComponentMoved(evt);
            }
            public void componentResized(java.awt.event.ComponentEvent evt) {
                formComponentResized(evt);
            }
        });

        menuView.setText("View");

        menuViewOSM.setText("OpenStreetMap");
        menuViewOSM.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                menuViewOSMActionPerformed(evt);
            }
        });
        menuView.add(menuViewOSM);

        menuViewTrack.setText("Track");
        menuViewTrack.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                menuViewTrackActionPerformed(evt);
            }
        });
        menuView.add(menuViewTrack);

        menuViewTrackExtrude.setText("Track extrude");
        menuViewTrackExtrude.addChangeListener(new javax.swing.event.ChangeListener() {
            public void stateChanged(javax.swing.event.ChangeEvent evt) {
                menuViewTrackExtrudeStateChanged(evt);
            }
        });
        menuView.add(menuViewTrackExtrude);
        menuView.add(jSeparator1);

        menuViewFollow.setText("Follow cursor");
        menuView.add(menuViewFollow);

        menuViewFPV.setText("First person view");
        menuView.add(menuViewFPV);

        menu.add(menuView);

        menuTools.setText("Tools");

        menuToolsMeasure.setText("Measure");
        menuToolsMeasure.setEnabled(false);
        menuTools.add(menuToolsMeasure);

        menu.add(menuTools);

        setJMenuBar(menu);

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGap(0, 627, Short.MAX_VALUE)
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGap(0, 469, Short.MAX_VALUE)
        );

        java.awt.Dimension screenSize = java.awt.Toolkit.getDefaultToolkit().getScreenSize();
        setBounds((screenSize.width-643)/2, (screenSize.height-528)/2, 643, 528);
    }// </editor-fold>//GEN-END:initComponents

	private void menuViewTrackExtrudeStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_menuViewTrackExtrudeStateChanged
		RenderableLayer track = (RenderableLayer)wwd.getModel().getLayers().getLayerByName("Track");
		if (track == null)
			return;
		
		for (Renderable item : track.getRenderables()) {
			if (item instanceof Path)
				((Path)item).setExtrude(menuViewTrackExtrude.getState());
		}
	}//GEN-LAST:event_menuViewTrackExtrudeStateChanged

	private void menuViewOSMActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_menuViewOSMActionPerformed
		updateLayersVisibility();
	}//GEN-LAST:event_menuViewOSMActionPerformed

	private void menuViewTrackActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_menuViewTrackActionPerformed
		updateLayersVisibility();
	}//GEN-LAST:event_menuViewTrackActionPerformed

	private void formWindowClosed(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_formWindowClosed
		WorldWind.shutDown();
		getContentPane().remove(wwd);
		wwd = null;		
	}//GEN-LAST:event_formWindowClosed

	private void formComponentMoved(java.awt.event.ComponentEvent evt) {//GEN-FIRST:event_formComponentMoved
		listener.actionPerformed(new ActionEvent(this, 0, "moved"));
	}//GEN-LAST:event_formComponentMoved

	private void formComponentResized(java.awt.event.ComponentEvent evt) {//GEN-FIRST:event_formComponentResized
		listener.actionPerformed(new ActionEvent(this, 1, "resized"));
	}//GEN-LAST:event_formComponentResized


    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JCheckBoxMenuItem menuViewFPV;
    private javax.swing.JCheckBoxMenuItem menuViewFollow;
    private javax.swing.JCheckBoxMenuItem menuViewOSM;
    private javax.swing.JCheckBoxMenuItem menuViewTrack;
    private javax.swing.JCheckBoxMenuItem menuViewTrackExtrude;
    // End of variables declaration//GEN-END:variables

	
	private void initWWD() {
        // Specify the configuration file for the elevation model prior to starting World Wind:
        Configuration.setValue(AVKey.EARTH_ELEVATION_MODEL_CONFIG_FILE,
            homeDir + "/data/config/CustomElevationModel.xml");
		
		wwd = new WorldWindowGLCanvas();
		this.getContentPane().setLayout(new BorderLayout());
		this.getContentPane().add(wwd, BorderLayout.CENTER);
		
		Model m = (Model)WorldWind.createConfigurationComponent(AVKey.MODEL_CLASS_NAME);
		
		StatusLayer status = new StatusLayer();
		status.setEnabled(true);
		status.setEventSource(wwd);
		m.getLayers().add(status);
		
		m.getLayers().getLayerByName("Place Names").setPickEnabled(false);

		wwd.setModel(m);				
	}
	
	
	private void updateLayersVisibility() {
		if (wwd == null)
			return;
		
		Layer track = wwd.getModel().getLayers().getLayerByName("Track");
		
		if (track != null)
			track.setEnabled(menuViewTrack.getState());
		
		Layer osm = wwd.getModel().getLayers().getLayerByName("OpenStreetMap");
		Layer landsat = wwd.getModel().getLayers().getLayerByName("Landsat");
		
		if (osm != null && landsat != null) {
			osm.setEnabled(menuViewOSM.getState());
			landsat.setEnabled(!menuViewOSM.getState());
		}
			
		wwd.redraw();
	}


	private void printLayers() {
		final LayerList layers = wwd.getModel().getLayers();
		for (Layer layer : layers) {
			System.out.println(
					layer.getName() + 
					(layer.isEnabled() ? " (enabled)" : " (disabled)") + 
					(layer.isPickEnabled() ? " (pickable)" : " (not pickable)"));
		}		
	}
	
	
	private void lookToTrack(boolean smooth) {
		if (wwd == null || !viewGuard.isOk() || !(wwd.getView() instanceof BasicOrbitView) || trackBounds == null)
			return;
		
		BasicOrbitView view = (BasicOrbitView)wwd.getView();
		Globe globe = wwd.getModel().getGlobe();
		
		Rectangle viewportRectangle = view.getViewport();
		Angle horizontalFieldOfView = view.getFieldOfView();
		Angle verticalFieldOfView = 
				Angle.fromDegrees((double)viewportRectangle.height / (double)viewportRectangle.width * horizontalFieldOfView.degrees);

		double lonAngle = trackBounds.maxLon - trackBounds.minLon;
		double latAngle = trackBounds.maxLat - trackBounds.minLat;
		double latCenter = trackBounds.minLat + latAngle / 2;
		double lonCenter = 0;
		if (lonAngle > 180) {
			lonAngle = 360 - lonAngle;
			lonCenter = (trackBounds.maxLon + lonAngle / 2) % 360;
		} else
			lonCenter = (trackBounds.minLon + lonAngle / 2);

		double earthRadius = globe.getRadiusAt(Angle.fromDegrees(latCenter), Angle.fromDegrees(lonCenter)); 
		double radius = earthRadius + trackBounds.maxAlt;

		double angleAtLatitude = lonAngle * Math.abs(Math.cos(Math.toRadians(latCenter)));
		double elevationH = radius * Math.sin(Math.toRadians(angleAtLatitude / 2)) / Math.sin(horizontalFieldOfView.radians / 2);
		double elevationV = radius * Math.sin(Math.toRadians(latAngle / 2)) / Math.sin(verticalFieldOfView.radians / 2);
		double elevation = Math.max(elevationH, elevationV);

		Position destination = new Position(Angle.fromDegrees(latCenter), Angle.fromDegrees(lonCenter), elevation);

		view.stopAnimations();
		
		if (smooth) {
			view.addPanToAnimator(
					view.getCenterPosition(), destination, view.getHeading(), Angle.fromDegrees(0), 
					view.getPitch(), Angle.fromDegrees(0), view.getZoom(), elevation, true);
		} else {
			view.setCenterPosition(Position.fromDegrees(latCenter, lonCenter));
			view.setZoom(elevation);
		}
	}
	
	/**
	 * Inserts layer before PlaceNames layer for right rendering order.
	 */
	private void insertLayer(Layer layer) {
		LayerList list = wwd.getModel().getLayers();
		list.add(list.indexOf(list.getLayerByName("Place Names")), layer);
	}

	private class ViewGuard {
		public ViewGuard() {
			initialized = false;
			wwd.addRenderingListener(new RenderingListener(){
				@Override
				public void stageChanged(RenderingEvent event) {
					if (event.getStage().equals(RenderingEvent.AFTER_BUFFER_SWAP)) {
						wwd.removeRenderingListener(this);
						initialized = true;
						lookToTrack(false);
					}
				}
			});
		}
		
		public boolean isOk() {
			return initialized;
		}
		
		private boolean initialized;
	}

	private class Bounds {
		public Bounds(double minLat, double maxLat, double minLon, double maxLon, double maxAlt) {
			this.minLat = minLat;
			this.maxLat = maxLat;
			this.minLon = minLon;
			this.maxLon = maxLon;
			this.maxAlt = maxAlt;
		}
		public final double minLat;
		public final double maxLat;
		public final double minLon;
		public final double maxLon;
		public final double maxAlt;
	}
	
	
	private WorldWindowGLCanvas wwd;
	private ViewGuard viewGuard;
	private boolean firstPersonView;
	private Bounds trackBounds;	
	
	private String latitude;
	private String longitude;
	private String altitude;
	private double offset;
	private String roll;
	private String pitch;
	private String heading;
	
	private final String homeDir;
	private final ActionListener listener;
}

